package avriServer.server;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import avriServer.channel.Channel;
import avriServer.channel.NonexistantChannelException;
import avriServer.channel.NonexistantParentChannelException;
import avriServer.channel.WrongChannelNameException;
import avriServer.client.NonexistantWaiterException;
import avriServer.client.Waiter;
import avriServer.command.CommandBuffer;
import avriServer.json.JSONException;
import avriServer.obj.Cleaner;
import avriServer.obj.CleanerChannel;
import avriServer.util.ErrorDisconnectionException;
import avriServer.util.IdentificationException;

import java.util.concurrent.Semaphore;

public class Dispatcher {
	private static ListChannel listChannel = new ListChannel();
	private static Map<String, Waiter> waiters = new HashMap<String, Waiter>();
	private static Semaphore semaphore = new Semaphore(1);
	private static AvriServer server ;
	static void setServer (AvriServer _server) {
		Dispatcher.server = _server;
	}
	public static AvriServer getServer () {
		return Dispatcher.server ;
	}
	/**
	 * return the Channel with the same absoluteNameas _channel
	 * <p>
	 * e.g: "general/myChannel/channel1"<br />
	 * <code>
         *      Dispatcher.getChannel(Channel.create('general/myChannel')); //return the channel general/myChannel
         * </code>
	 * 
	 * @param _channel
	 * @return the channel
	 * @throws NonexistantChannelException
	 * @throws WrongChannelNameException 
	 */
	public static synchronized Channel getChannel(String _channelName)
			throws NonexistantChannelException {
		return Dispatcher.listChannel.getChannel(_channelName);
	}

	/**
	 * Add the waiter _waiter to the Dispatcher.
	 * @param _waiter the waiter to add
	 */
	
	public static synchronized void addWaiter (Waiter _waiter) {
		Dispatcher.waiters.put(_waiter.getPublicId(), _waiter);
	}

	
	/**
	 * Move the channel with the absoluteName _channelAbsoluteName to the new path _newPath. _newPath should be
	 * the name WITHOUT the _channel.name e.g: if you want to move the channel
	 * _channel general/myChannel/channel1 into private/myChannel/channel1
	 * <code>
     *      Dispatcher.moveChannel("general/myChannel/channel1", "private/myChannel")
     *</code>
	 * 
	 * @param _channel
	 * @param _channelNewAbsoluteName
	 */
	public static synchronized void moveChannel(String _channelAbsoluteName, String _newPath)
			throws NonexistantChannelException, NonexistantParentChannelException, WrongChannelNameException {
		//check if the _newPath is all right
		Channel.IS_VALID_NAME(_newPath);
		
		Channel theChannel = Dispatcher.getChannel(_channelAbsoluteName);
		theChannel.setAbsoluteName(_newPath);
		//add it at the right location
		Dispatcher.addChannel(theChannel);
		//remove the channel of the list
		Channel parentOldChannel = theChannel.getParentChannel();
		//first-level channel
		if (parentOldChannel == null)
			Dispatcher.listChannel.removeChannel(_channelAbsoluteName) ;
		else
			parentOldChannel.removeChannel(_channelAbsoluteName);			
	}
	
	public static synchronized void addWaiterToChannel(String _waiterPublicId, String _channelAbsoluteName) 
		throws NonexistantChannelException, NonexistantWaiterException {
		Dispatcher.getChannel(_channelAbsoluteName).addWaiter(Dispatcher.getWaiter(_waiterPublicId));
	}

	/**
	 * Remove the channel _channel using the absoluteName. e.g:
	 * Dispatcher.remove(Channel.create("general/myChannel/channel1")) will
	 * remove channel1
	 * 
	 * @param _channel
	 * @param _channelNewAbsoluteName
	 * @throws NonexistantChannelException
	 */
	public static synchronized void removeChannel(String _channelName)
			throws NonexistantChannelException {
		Channel channelToRemove = Dispatcher.getChannel(_channelName);
		Channel parentChannel = channelToRemove.getParentChannel();
		//parentChannel == null => it's a first level channel
		if (parentChannel == null) {
			Dispatcher.listChannel.removeChannel(_channelName);
		}
		else
			parentChannel.removeChannel(_channelName);
	}

	/**
	 * add the _channel Channel to the list of the channel. If the absoluteName
	 * of the channel is set, it will directly add the channel into the right
	 * sub-channel. e.g: "general/myChannel/channel1" <code>
         *       Dispatcher.addChannel(new Channel("general/myChannel/channel1")) // create the channel 'channel1' into 'general/myChannel/' !
         *</code>
	 * 
	 * If one of the absoluteName's channel is inexistent (general/ or
	 * general/myChannel/), it will throw an UnexistantChannelException error.
	 * If it's a first-level Channel, it will be directly inserted.
	 * _channel.parentChannel is automatically set. if a channel with the same
	 * absoluteName already exists, it will be replaced
	 * 
	 * @param _channel
	 * @throws NonexistantChannelException 
	 * @throws NonexistantChannelException If one of the absoluteName's channel is inexistent 
	 */
	public static synchronized void addChannel(Channel _channel) 
		throws NonexistantParentChannelException {
		Channel parentChannel;
			try {
				parentChannel = Dispatcher.findParentChannel(_channel.getAbsoluteName());
			} catch (NonexistantChannelException e) {
				throw new NonexistantParentChannelException();
			}
		if (parentChannel == null)
			Dispatcher.listChannel.addChannel(_channel);
		else
			parentChannel.addChannel(_channel);
	}

	/**
	 * Find the parentChannel using the _absoluteName.
	 * So if _channel.absoluteName == 'general/myChannel/channel1', it will return the channel 'general/myChannel'
	 * (doesn't care about the _channel.parentChannel value).
	 * @param _absoluteName
	 * @return null if it's a first-level channel
	 * @throws NonexistantChannelException
	 */
	public static synchronized Channel findParentChannel (String _absoluteName) throws NonexistantChannelException {
		int indexOfSeparator = _absoluteName.lastIndexOf("/");
		if (indexOfSeparator == -1) {
			return null ;
		}
		else {
			String channelParentAbsoluteName = _absoluteName.substring(0, indexOfSeparator);
			return Dispatcher.listChannel.getChannel(channelParentAbsoluteName) ;
		}
	}

	/**
	 * Subscribe the waiter _waiter to the channel _channel.
	 * Return the number of waiter on this channel.
	 * @param _channel
	 * @param _waiter
	 * @return number of Channel
	 * @throws NonexistantChannelException
	 */
	public static synchronized int subscribe(String _channelAbsoluteName, Waiter _waiter)
			throws NonexistantChannelException {
		Channel theChannel;
		theChannel = Dispatcher.getChannel(_channelAbsoluteName);
		_waiter.subscribe(theChannel);
		return theChannel.getNumberOfWaiter();
	}

	/**
	 * Unsubscribe the waiter _waiter to the channel _channel.
	 * 
	 * @param _channel
	 * @param _name
	 * @param _waiter
	 * @return 
	 * @throws NonexistantChannelException 
	 * @throws NonexistantChannelException
	 */
	public static synchronized int unsubscribe(String _channelAbsoluteName, Waiter _waiter) 
			throws NonexistantChannelException {
		Channel theChannel;
		theChannel = Dispatcher.getChannel(_channelAbsoluteName);
		_waiter.unsubscribe(theChannel);
		return theChannel.getNumberOfWaiter();
	}

	/**
	 * Push the function _functionName with its attributes _attributes to all the user of all the channels
	 * e.g: 
	 * <code>
	 * 		Dispatcher.push("error", "message1", "foo") will push the message {functionName: "error", arguments: ["message1", "foo"]}
	 * 		//Javascript api will automatically launch the function : avriServer.functions.error ("message1", "foo") ;
	 * </code>		
	 * Also work with Dispatcher.push("myJavascriptSpaceName.error", "message1", "foo") : avriServer.functions.myJavascriptSpaceName.error ("message1", "foo")
	 * @param _channel
	 * @param _message
	 * @throws JSONException 
	 */
	public static synchronized void massivePush(String _functionName, Object... _arguments) {
		for (Entry<String, Waiter> entry : Dispatcher.waiters.entrySet()) {
			entry.getValue().push(_functionName, _arguments);
		}
	}

	
	/**
	 * Push the commandBuffer to all the user of all the channels
	 * e.g: 
	 * <code>
	 * 		Dispatcher.push("error", "message1", "foo") will push the message {functionName: "error", arguments: ["message1", "foo"]}
	 * 		//Javascript api will automatically launch the function : avriServer.functions.error ("message1", "foo") ;
	 * </code>		
	 * Also work with Dispatcher.push("myJavascriptSpaceName.error", "message1", "foo") : avriServer.functions.myJavascriptSpaceName.error ("message1", "foo")
	 * @param _channel
	 * @param _message
	 */
	public static synchronized void massivePush(CommandBuffer _commandBuffer) {
		String commands = new String(_commandBuffer.getCommandsAsPartOfArray());
		for (Entry<String, Waiter> entry : Dispatcher.waiters.entrySet()) {
			entry.getValue().push(commands);
		}
	}

	/**
	 * Push the function _functionName with its attributes _attributes to all the user of all the channels except to the waiter _waiter
	 * e.g: 
	 * <code>
	 * 		Dispatcher.push("error", "message1", "foo") will push the message {functionName: "error", arguments: ["message1", "foo"]}
	 * 		//Javascript api will automatically launch the function : avriServer.functions.error ("message1", "foo") ;
	 * </code>		
	 * Also work with Dispatcher.push("myJavascriptSpaceName.error", "message1", "foo") : avriServer.functions.myJavascriptSpaceName.error ("message1", "foo")
	 * @param _channel
	 * @param _message
	 * @param _nameExcept
	 */
	public static synchronized void massivePushExcept(Waiter _waiter, String _functionName, Object... _arguments) {
		for (Entry<String, Waiter> entry : Dispatcher.waiters.entrySet()) {
			if (entry.getValue() != _waiter)
				entry.getValue().push(_functionName, _arguments);
		}
	}

	/**
	 * Push the function _functionName with its attributes _attributes to all the user of all the channels except to the waiter _waiter
	 * e.g: 
	 * <code>
	 * 		Dispatcher.push("error", "message1", "foo") will push the message {functionName: "error", arguments: ["message1", "foo"]}
	 * 		//Javascript api will automatically launch the function : avriServer.functions.error ("message1", "foo") ;
	 * </code>		
	 * Also work with Dispatcher.push("myJavascriptSpaceName.error", "message1", "foo") : avriServer.functions.myJavascriptSpaceName.error ("message1", "foo")
	 * @param _channel
	 * @param _message
	 * @param _nameExcept
	 */
	public static synchronized void massivePushExcept(Waiter _waiter, CommandBuffer _commandBuffer) {
		String commands = new String(_commandBuffer.getCommandsAsPartOfArray());
		for (Entry<String, Waiter> entry : Dispatcher.waiters.entrySet()) {
			if (entry.getValue() != _waiter)
				entry.getValue().push(commands);
		}
	}

	/**
	 * Push the function _functionName with its attributes _attributes to all the user of the channel _channel
	 * e.g: 
	 * <code>
	 * 		Dispatcher.push("error", "message1", "foo") will push the message {functionName: "error", arguments: ["message1", "foo"]}
	 * 		//Javascript api will automatically launch the function : avriServer.functions.error ("message1", "foo") ;
	 * </code>		
	 * Also work with Dispatcher.push("myJavascriptSpaceName.error", "message1", "foo") : avriServer.functions.myJavascriptSpaceName.error ("message1", "foo")
	 * @param _channel
	 * @param _message
	 * @throws JSONException 
	 * @throws NonexistantChannelException 
	 */
	public static synchronized void pushInChannel(String _channelAbsoluteName, String _functionName, Object... _arguments) 
		throws NonexistantChannelException {
		Channel theChannel;
		theChannel = Dispatcher.getChannel(_channelAbsoluteName);
		for (Entry<String, Waiter> entry : theChannel.getWaiters().entrySet()) {
			entry.getValue().push(_functionName, _arguments);
		}
	}

	/**
	 * Push the function _functionName with its attributes _attributes to all the user of the channel _channel except to the waiter _waiterPublicId
	 * e.g: 
	 * <code>
	 * 		Dispatcher.push("error", "message1", "foo") will push the message {functionName: "error", arguments: ["message1", "foo"]}
	 * 		//Javascript api will automatically launch the function : avriServer.functions.error ("message1", "foo") ;
	 * </code>		
	 * Also work with Dispatcher.push("myJavascriptSpaceName.error", "message1", "foo") : avriServer.functions.myJavascriptSpaceName.error ("message1", "foo")
	 * @param _channel
	 * @param _message
	 * @throws JSONException 
	 * @throws NonexistantChannelException 
	 */
	public static synchronized void pushInChannelExcept(String _channelAbsoluteName, String _waiterPublicId, String _functionName, Object... _arguments) 
		throws NonexistantChannelException {
		Channel theChannel;
		theChannel = Dispatcher.getChannel(_channelAbsoluteName);
		for (Entry<String, Waiter> entry : theChannel.getWaiters().entrySet()) {
			if (!entry.getValue().getPublicId().equals(_waiterPublicId))
				entry.getValue().push(_functionName, _arguments);
		}
	}

	/**
	 * Push the function _functionName with its attributes _attributes to all the user of the channel _channel except to the waiter _waiter
	 * e.g: 
	 * <code>
	 * 		Dispatcher.push("error", "message1", "foo") will push the message {functionName: "error", arguments: ["message1", "foo"]}
	 * 		//Javascript api will automatically launch the function : avriServer.functions.error ("message1", "foo") ;
	 * </code>		
	 * Also work with Dispatcher.push("myJavascriptSpaceName.error", "message1", "foo") : avriServer.functions.myJavascriptSpaceName.error ("message1", "foo")
	 * @param _channel
	 * @param _message
	 * @param _nameExcept
	 * @throws NonexistantChannelException
	 */
	public synchronized void pushInChannelExcept(String _channelAbsoluteName, Waiter _waiter, String _functionName, Object... _arguments) throws JSONException, NonexistantChannelException {
		Channel theChannel;
		theChannel = Dispatcher.getChannel(_channelAbsoluteName);
		for (Entry<String, Waiter> entry : theChannel.getWaiters().entrySet()) {
			if (entry.getValue() != _waiter)
				entry.getValue().push(_functionName, _arguments);
		}
	}

	/**
	 * Push the function _functionName with its attributes _attributes to the waiter _waiter.
	 * Waiter is selected using his name (NOT session).
	 * e.g: 
	 * <code>
	 * 		Dispatcher.push("error", "message1", "foo") will push the message {functionName: "error", arguments: ["message1", "foo"]}
	 * 		//Javascript api will automatically launch the function : avriServer.functions.error ("message1", "foo") ;
	 * </code>		
	 * Also work with Dispatcher.push("myJavascriptSpaceName.error", "message1", "foo") : avriServer.functions.myJavascriptSpaceName.error ("message1", "foo")
	 * @param _channel
	 * @param _message
	 * @param _nameExcept
	 * @throws JSONException 
	 * @throws NonexistantChannelException
	 */
	public static synchronized void pushToWaiter(String _waiterPublicId, String _functionName, Object... _arguments)
		throws NonexistantWaiterException {
		Waiter theWaiter = Dispatcher.waiters.get(_waiterPublicId);
		if (theWaiter == null)
			throw new NonexistantWaiterException();
		theWaiter.push(_functionName, _arguments);
	}

	/**
	 * Push the function _functionName with its attributes _attributes to all the channels the waiter _waiter has subscribe. 
	 * Waiter is selected using his name (NOT session).
	 * e.g: 
	 * <code>
	 * 		Dispatcher.push("error", "message1", "foo") will push the message {functionName: "error", arguments: ["message1", "foo"]}
	 * 		//Javascript api will automatically launch the function : avriServer.functions.error ("message1", "foo") ;
	 * </code>		
	 * Also work with Dispatcher.push("myJavascriptSpaceName.error", "message1", "foo") : avriServer.functions.myJavascriptSpaceName.error ("message1", "foo")
	 * @param _channel
	 * @param _message
	 * @param _nameExcept
	 * @throws NonexistantChannelException
	 * @throws JSONException 
	 */
	public static synchronized void push(String _waiterPublicId, String _functionName, Object... _arguments)
		throws NonexistantChannelException, NonexistantWaiterException, JSONException {
		Waiter theWaiter = Dispatcher.waiters.get(_waiterPublicId);
		if (theWaiter == null)
			throw new NonexistantWaiterException();
		for (Channel channel : theWaiter.getSuscribedChannels()) {
			channel.push(_functionName, _arguments);
		}
	}

	/**
	 * Return the number of waiter in a channel
	 * 
	 * @param _channelName
	 * @return
	 * @throws NonexistantChannelException 
	 */
	public static synchronized int getNumberWaiter(String _channelName) throws NonexistantChannelException {
		return Dispatcher.getChannel(_channelName).getWaiters().size();
	}

	/**
	 * Return true if the Waiter with the name _waiterPublicId exists in the channel.
	 * @param _channelAbsoluteName
	 * @param _waiter
	 * @return
	 * @throws NonexistantChannelException 
	 */
	public static synchronized boolean containsWaiter(String _channelAbsoluteName, String _waiterPublicId) throws NonexistantChannelException {
		return Dispatcher.listChannel.getChannel(_channelAbsoluteName).getWaiters().containsKey(_waiterPublicId) ;
	}

	/**
	 * Return the waiter _waiter (with the same name) in the channel _channel
	 * 
	 * @param _channel
	 * @param _waiter
	 * @return the waiter
	 * @throws UserDisconnectedException
	 * @throws NonexistantChannelException 
	 */
	public static synchronized Waiter getWaiterIntoChannel(String _channelAbsoluteName, String _waiterPublicId)
			throws NonexistantWaiterException, NonexistantChannelException {
		return Dispatcher.listChannel.getChannel(_channelAbsoluteName).getWaiter(_waiterPublicId);
	}

	/**
	 * Return the waiter with the same name. Look on every channel
	 * 
	 * @param _name
	 * @return
	 * @throws NonexistantWaiterException 
	 */
	public static synchronized Waiter getWaiter(String _waiterPublicId) throws NonexistantWaiterException {
		if (!Dispatcher.waiters.containsKey(_waiterPublicId))
			throw new NonexistantWaiterException();
		return Dispatcher.waiters.get(_waiterPublicId) ;
	}

	/**
	 * Disconnect the waiter _waiter from the server.
	 * Look on all the channel and destroy it
	 * It will also call the method disconnect() of the Waiter _waiter
	 * 
	 * @param _clientId
	 * @return True if there was a worker, false if no worker found.
	 * @throws NonexistantChannelException
	 * @throws NonexistantWaiterException 
	 */
	public static synchronized boolean disonnectWaiter(String _waiterPublicId) {
		Waiter waiter;
		try {
			waiter = Dispatcher.getWaiter(_waiterPublicId);
			waiter.actionExecutedBeforedisconnection();
			waiter.unsubscribeFromAllChannel();
			Dispatcher.waiters.remove(_waiterPublicId);
			return true ;
		} catch (NonexistantWaiterException e) {
			return false ;
		}
	}

	/**
	 * remove all the inactive Waiters. Called by the CleanerChannel Object.
	 */
	public static synchronized void clean() {
		ArrayList<Waiter> listDestroyWaiter = new ArrayList<Waiter>();
		for (Entry<String, Waiter> entry : Dispatcher.waiters.entrySet()) {
			if (!entry.getValue().isKeptAlive()) {
				listDestroyWaiter.add(entry.getValue());
			}
		}
		// Now we remove the waiter
		for (Waiter waiter: listDestroyWaiter) {
			Dispatcher.disonnectWaiter(waiter.getPublicId());
		}
	}

	/**
	 * Create and return a session key.
	 * @return String uniqueSession
	 */
	public static synchronized String generateWaiterSession() {
		int length = server.getCharacterSession().length();
		StringBuffer session = new StringBuffer();
		int index = 0;
		for (int i = 0; i < server.getSizeSession(); i++) {
			index = (int) Math.round(Math.random() * length);
			if (index == length)
				index--;
			session.append(server.getCharacterSession().substring(index, index + 1));
		}
		return session.toString();
	}
	
	/**
	 * Remove the TemporaryChannels from the server if there is no waiter inside.
	 */
	public static synchronized void cleanChannel() {
		Dispatcher.listChannel.cleanVacantChannel();
	}

	/**
	 * Get the waiter and check the session.
	 * @param _waiterPublicId
	 * @param _session
	 * @return
	 * @throws ErrorConnectionException
	 * @throws NonexistantWaiterException
	 */
	public static synchronized Waiter getWaiter (String _waiterPublicId, String _session)
			throws IdentificationException, NonexistantWaiterException {
		Waiter w = Dispatcher.getWaiter(_waiterPublicId);
		if (!w.getSession().equals(_session))
			throw new IdentificationException();
		return w;
	}
	
	/**
	 * Keep alive the waiter _waiter
	 * @param _waiter
	 * @throws NonexistantWaiterException
	 */
	public static synchronized void keepAlive(String _waiterPublicId, String _session)
			throws NonexistantWaiterException {
		Waiter w = Dispatcher.getWaiter(_waiterPublicId);
		if (w != null && w.getSession().equals(_session))
			w.keepAlive();
		else
			throw new NonexistantWaiterException();
	}

	/**
	 * Lock the Dispatcher. The dispatcher semaphore is acquired. Should obligatory be used with unlock() method.
	 * 
	 * @throws InterruptedException
	 */
	public static void lock() throws InterruptedException {
		Dispatcher.semaphore.acquire();
	}

	/**
	 * Unlock the Dispatcher. The dispatcher semaphore is released.
	 */
	public static void unlock() {
		Dispatcher.semaphore.release();
	}
	
	/**
	 * Character of the Public Id are Server.CHARACTER_NAME, size is Server.SIZE_NAME
	 * @return an unique Public Id for a waiter.
	 */
	public static synchronized String generateUniqueWaiterPublicId () {
		int length = server.getCharacterPublicId().length();
		StringBuffer name = new StringBuffer();
		int index = 0;
		for (int i = 0; i < server.getSizePublicId(); i++) {
			index = (int) Math.round(Math.random() * length);
			if (index == length)
				index--;
			name.append(server.getCharacterPublicId().substring(index, index + 1));
		}
		String theName = name.toString() ;
		if(Dispatcher.waiters.containsKey(theName))
			theName = Dispatcher.generateUniqueWaiterPublicId();
		return theName;	
	}
	
	/**
	 * @return all the waiters registered into the Dispatcher <PublicId, Waiter>
	 */
	public static synchronized Map<String, Waiter> getWaiters () {
		return Dispatcher.waiters ;
	}
}
